package gui;

/*
 * Author: Peter Perera
 * Description: Neural Network - Feedforward with Backpropagation
 */

import info.monitorenter.gui.chart.Chart2D;
import info.monitorenter.gui.chart.ITrace2D;
import info.monitorenter.gui.chart.traces.Trace2DSimple;

import java.awt.Color;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.util.Date;
import java.util.Vector;

import javax.swing.JFrame;

public class NNetwork {

	// Configuration
	private static int numInputs = 320;
	private static int numHiddenNodes = 6;
	private static int numOutputs = 10;
	
	private static int numEpochs = 9;
	
	private static double learningRate = 0.01;
	
	// Processing variables
	private static double[][] weights_IH = new double[numHiddenNodes][numInputs];
	private static double[][] weights_HO = new double[numOutputs][numHiddenNodes];
	
	private static double[] inputs = new double[numInputs+1];
	private static double[] hidden = new double[numHiddenNodes];
	private static double[] outputs = new double[numOutputs];
	
	private static int[][] trainInputs;
	private static int[][] trainDesiredOutputs;
	private static int[][] validationInputs;
	private static int[][] validationDesiredOutputs;
	private static int[][] testInputs;
	private static int[][] testDesiredOutputs;
	
	private static double outputError;
	
	private static double[] outputNeuronError = new double[numOutputs];
	
	private static double RMSerror;
	private static double RMSValError;
	
	private static double[] RMSerrorlog = new double[numEpochs];
	private static double[] RMSValErrorLog = new double[numEpochs];
	
	
	private static String[] randomInputTracker;
	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		
		System.out.println("Loading Neural Network");
		NNetwork.initWeights();
		NNetwork.loadInputs();
		System.out.println("Neural Network Loading...[Complete]");
		
		System.out.print("Begin Neural Network Training...");
		NNetwork.train();
		System.out.println("[Complete]");
		
		graphResults();
	}
	
	
	public static void calcOverallError() {
	     RMSerror = 0.0;
	     for(int i = 0;i<trainInputs.length;i++)
	     {
	        NNetwork.calcOutput(i, trainInputs, trainDesiredOutputs);
	        RMSerror = RMSerror + (outputError * outputError);
	     }
	     RMSerror = RMSerror/trainInputs.length;
	     RMSerror = java.lang.Math.sqrt(RMSerror);
	}
	

	public static void calcOverallValError() {
		RMSValError = 0.0;
		for(int i = 0;i<validationInputs.length;i++)
		{
			NNetwork.calcOutput(i, validationInputs, validationDesiredOutputs);
			RMSValError = RMSValError + (outputError * outputError);
		}
		RMSValError = RMSValError/10;
		RMSValError = java.lang.Math.sqrt(RMSValError);
	    	
	}
	
	public static void train() {
		boolean debug = false;
		
		for(int epochNum = 0 ; epochNum < numEpochs ; epochNum++)
		{
			int inputNum=0;
			
			// Initialise Random Input
			randomInputTracker = new String[trainInputs.length];
			
			for(int i = 0; i< trainInputs.length; i++)
			{
				randomInputTracker[i] = "notUsedYet";
			}
			for(int[] input: trainInputs)
			{
				inputNum = randomiseInput(trainInputs);
				NNetwork.calcOutput(inputNum,trainInputs,trainDesiredOutputs);		// calc output and error (feedforward)
				if (debug ||Debug.DEBUG) System.out.println("\toutputError : "+ outputError + "\n\t" + Debug.printArray(hidden) +"\n\t"+Debug.printArray(outputs));
				
				NNetwork.updateWeights(inputNum);	// update weights (backprop)
				inputNum++;
			}
			
			NNetwork.calcOverallError();	// calc overall network error
			NNetwork.calcOverallValError(); // calc overall validation error
			
			RMSerrorlog[epochNum] = RMSerror;
			RMSValErrorLog[epochNum] = RMSValError;
		}
	}
	
	
	private static int randomiseInput(int[][] input) {

		
		int newInputNum = (int) Math.random() * input.length;
		
		while(randomInputTracker[newInputNum] == null)
		{
			if(randomInputTracker[newInputNum].contains("notUsedYet"))
			{
				randomInputTracker[newInputNum] = null;
			} else {
				newInputNum = (int) Math.random() * input.length;
			}
		}
		
		
		return 0;
	}


	//TODO:: Check Weight Update
	private static void updateWeights(int inputNum) {
		
		// Hidden-Output Layer Weights
		for(int outputNum = 0; outputNum< numOutputs ; outputNum++)
		{
			for(int hiddenNum = 0; hiddenNum < numHiddenNodes; hiddenNum++)
			{
				double weightChange = 0.0;
				weightChange = learningRate * outputNeuronError[outputNum] * hidden[hiddenNum];
				weights_HO[outputNum][hiddenNum] += weightChange;
			}
		}
		
		// Input-Hidden Layer Weights
		for(int hiddenNum = 0 ; hiddenNum < numHiddenNodes; hiddenNum++)
		{
			for(int inputNumSel = 0 ; inputNumSel < numInputs; inputNumSel++)
			{
				double weightChange = 0.0;
				for (int outputNum = 0; outputNum< numOutputs ; outputNum++)
				{
					weightChange += outputNeuronError[outputNum] * weights_HO[outputNum][hiddenNum];
				}
				weightChange = weightChange * trainInputs[inputNum][inputNumSel] * learningRate;
				weights_IH[hiddenNum][inputNumSel] += weightChange;
				
			}
		}
		
		
	}

	public static void calcOutput(int inputNum, int[][] inputs, int[][] desired) {
		for(int i = 0;i<numHiddenNodes;i++)
		{
			hidden[i] = 0.0;

			for(int j = 0;j<numInputs;j++)
				hidden[i] = hidden[i] + (inputs[inputNum][j] * weights_IH[i][j]);

			hidden[i] = tanh(hidden[i]);
		}
		
		for(int i = 0;i<numOutputs;i++)
		{
			outputs[i] = 0.0;

			for(int j = 0;j<numHiddenNodes;j++)
				outputs[i] = outputs[i] + (hidden[j] * weights_HO[i][j]);

			outputs[i] = tanh(outputs[i]);
		}
		
		outputError = 0;
		for(int i = 0;i<numOutputs;i++)
		{
			outputNeuronError[i] =  Math.pow(desired[inputNum][i] - outputs[i], 2);
			outputError += outputNeuronError[i];
		}
		outputError = outputError * 0.5;
		
	}

	public static double tanh(double x)
	{
		if (x > 20)
	        return 1;
	    else if (x < -20)
	        return -1;
	    else
	        {
	        double a = Math.exp(x);
	        double b = Math.exp(-x);
	        return (a-b)/(a+b);
	        }
	}

	public static void initWeights()
	{
		boolean debug = false;
		
		System.out.print("\tInitialising Weights");
		for(int hiddenNode = 0; hiddenNode < weights_IH.length; hiddenNode++)
		{
			for(int connWeight = 0; connWeight < weights_IH[hiddenNode].length ; connWeight++)
			{
				weights_IH[hiddenNode][connWeight] = (Math.random())/2;
			}
		}
		System.out.print(".");
		for(int outputNode = 0; outputNode < weights_HO.length; outputNode++)
		{
			for(int connWeight = 0; connWeight < weights_HO[outputNode].length ; connWeight++)
			{
				weights_HO[outputNode][connWeight] = (Math.random())/2;
			}
		}
		if (debug || Debug.DEBUG) System.out.println("Initialised Weights\n" + Debug.printArray(weights_IH) + "\n\n" + Debug.printArray(weights_HO));
		
		System.out.println("..[Done]");
	}
	
	public static void loadInputs()
	{
		boolean debug = false;
		
		Vector<String> filesTraining = new Vector<String>();
		Vector<String> filesValidation = new Vector<String>();
		Vector<String> filesTesting = new Vector<String>();
		
		Vector<String> files = new Vector<String>();
		
		
		System.out.print("\tLoading Inputs");
		
		//load all training files
		File samplePath = new File(System.getProperty("user.dir") + "\\src\\samples"); // Location of samples
		String[] sampleDirList = samplePath.list(); // List of sample directory contents
			 
		for(int i=0; i<sampleDirList.length; i++){
			
			File file = new File(samplePath, sampleDirList[i]);
				
				if(file.isDirectory()){
					// Do nothing if a Directory
				} else {
					if (file.getName().contains("train"))
					{
						filesTraining.add(file.getAbsolutePath());
						
						if (debug || Debug.DEBUG) System.out.println("\t" + file.getAbsolutePath());
					} else if (file.getName().contains("test"))
					{
						filesTesting.add(file.getAbsolutePath());
					} else if (file.getName().contains("validation")) {
						filesValidation.add(file.getAbsolutePath());
					} else 
					{
						System.err.println("unknown input set encountered");
					}
					files.add(file.getAbsolutePath());
				}
		 }
		
		 // Initialise Variables
		trainInputs = new int[filesTraining.size()][numInputs];
		trainDesiredOutputs = new int[filesTraining.size()][numOutputs]; 
		
		validationInputs = new int[filesValidation.size()][numInputs];
		validationDesiredOutputs = new int[filesValidation.size()][numOutputs];
		testInputs = new int[filesTesting.size()][numInputs];
		testDesiredOutputs = new int[filesTesting.size()][numOutputs];
		
		
		// load Training Set
		 int maxFiles = files.size();
		 int numTrainingPatterns = 0; 
		 for(String trainingSampleLoc: filesTraining)
		 {
			 int inputNum = 0;
			 NeuralNetworkSet patternData = Filesystem.readData(trainingSampleLoc);
	
			 for (int[] y : patternData.getInput())
			 {
				 for (int x : y)
				 {
					 trainInputs[numTrainingPatterns][inputNum] = x;
					 inputNum++;
				 }
			 }
			 
			 for(int desOutputNum = 0;desOutputNum<numOutputs; desOutputNum++)
			 {
				 if(patternData.getDesired() == desOutputNum)
				 {
					 trainDesiredOutputs[numTrainingPatterns][desOutputNum] = 1;
				 } else {
					 trainDesiredOutputs[numTrainingPatterns][desOutputNum] = -1;
				 }
			 } 
			 numTrainingPatterns++;
		 }
		 
		 // load Validation Set
		 int numValidationPatterns = 0; 
		 for(String validationSampleLoc: filesValidation)
		 {
			 int inputNum = 0;
			 NeuralNetworkSet patternData = Filesystem.readData(validationSampleLoc);
	
			 for (int[] y : patternData.getInput())
			 {
				 for (int x : y)
				 {
					 trainInputs[numValidationPatterns][inputNum] = x;
					 inputNum++;
				 }
			 }
			 
			 for(int desOutputNum = 0;desOutputNum<numOutputs; desOutputNum++)
			 {
				 if(patternData.getDesired() == desOutputNum)
				 {
					 validationDesiredOutputs[numValidationPatterns][desOutputNum] = 1;
				 } else {
					 validationDesiredOutputs[numValidationPatterns][desOutputNum] = -1;
				 }
			 } 
			 numValidationPatterns++;
		 }
	 
		 // load Test Set
		 int numTestPatterns = 0; 
		 for(String testSampleLoc: filesTesting)
		 {
			 int inputNum = 0;
			 NeuralNetworkSet patternData = Filesystem.readData(testSampleLoc);
	
			 for (int[] y : patternData.getInput())
			 {
				 for (int x : y)
				 {
					 trainInputs[numTestPatterns][inputNum] = x;
					 inputNum++;
				 }
			 }
			 
			 for(int desOutputNum = 0;desOutputNum<numOutputs; desOutputNum++)
			 {
				 if(patternData.getDesired() == desOutputNum)
				 {
					 testDesiredOutputs[numTestPatterns][desOutputNum] = 1;
				 } else {
					 testDesiredOutputs[numTestPatterns][desOutputNum] = -1;
				 }
			 } 
			 numTestPatterns++;
		 }
		 System.out.println("...[Done]");
	}



    public static void graphResults()
    {
		// Create a chart:  
		Chart2D chart = new Chart2D();
		// Create an ITrace: 
		ITrace2D errorCurve = new Trace2DSimple(); 
		ITrace2D validationCurve = new Trace2DSimple();
		// Add all points, as it is static: 
		
		for(int i=0;i<RMSerrorlog.length;i++){
			errorCurve.addPoint(i,RMSerrorlog[i]);
		}

		for(int i=0; i<RMSValErrorLog.length; i++)
		{
			validationCurve.addPoint(i, RMSValErrorLog[i]);
		}
		validationCurve.setName("Validation Curve");
		validationCurve.setColor(new Color(58,131,220));
		
		
		errorCurve.setName("Error Curve");
		errorCurve.setColor(new Color(255,175,50));
		
		
		// Add the trace to the chart: 
		chart.addTrace(errorCurve);  
		//chart.addTrace(validationCurve);
		
		

		// Make it visible:
		// Create a frame.
		Date time = new Date();
		JFrame frame = new JFrame(time.toString());
		// add the chart to the frame: 
		frame.getContentPane().add(chart);
		frame.setSize(800,600);
		// Enable the termination button [cross on the upper right edge]: 
		frame.addWindowListener(
				new WindowAdapter(){
					public void windowClosing(WindowEvent e){
						System.exit(0);
					}
				}
		);
		frame.show();
    }
    
}
	



